#region

using System;
using System.IO;
using TvdbLib.ICSharpCode.SharpZipLib.Zip;

#endregion

namespace TvdbLib.SharpZipLib.Zip
{
    /// <summary>
    ///     Holds data pertinent to a data descriptor.
    /// </summary>
    public class DescriptorData
    {
        #region Fields

        private long crc;

        #endregion Fields

        #region Properties

        /// <summary>
        ///     Get /set the compressed size of data.
        /// </summary>
        public long CompressedSize { get; set; }

        /// <summary>
        ///     Get /set the crc value.
        /// </summary>
        public long Crc
        {
            get { return crc; }
            set { crc = (value & 0xffffffff); }
        }

        /// <summary>
        ///     Get / set the uncompressed size of data
        /// </summary>
        public long Size { get; set; }

        #endregion Properties
    }

    internal class EntryPatchData
    {
        #region Fields

        #endregion Fields

        #region Properties

        public long CrcPatchOffset { get; set; }

        public long SizePatchOffset { get; set; }

        #endregion Properties
    }

    /// <summary>
    ///     This class assists with writing/reading from Zip files.
    /// </summary>
    internal class ZipHelperStream : Stream
    {
        #region Fields

        private bool isOwner_;
        private Stream stream_;

        #endregion Fields

        #region Constructors

        /// <summary>
        ///     Initialise an instance of this class.
        /// </summary>
        /// <param name="name"> The name of the file to open. </param>
        public ZipHelperStream(string name)
        {
            stream_ = new FileStream(name, FileMode.Open, FileAccess.ReadWrite);
            isOwner_ = true;
        }

        /// <summary>
        ///     Initialise a new instance of <see cref="ZipHelperStream" />.
        /// </summary>
        /// <param name="stream"> The stream to use. </param>
        public ZipHelperStream(Stream stream)
        {
            stream_ = stream;
        }

        #endregion Constructors

        #region Properties

        public override bool CanRead
        {
            get { return stream_.CanRead; }
        }

        public override bool CanSeek
        {
            get { return stream_.CanSeek; }
        }

        public override bool CanWrite
        {
            get { return stream_.CanWrite; }
        }

        /// <summary>
        ///     Get / set a value indicating wether the the underlying stream is owned or not.
        /// </summary>
        /// <remarks>
        ///     If the stream is owned it is closed when this instance is closed.
        /// </remarks>
        public bool IsStreamOwner
        {
            get { return isOwner_; }
            set { isOwner_ = value; }
        }

        public override long Length
        {
            get { return stream_.Length; }
        }

        public override long Position
        {
            get { return stream_.Position; }
            set { stream_.Position = value; }
        }

        #endregion Properties

#if !NET_1_0 && !NET_1_1 && !NETCF_1_0

        public override bool CanTimeout
        {
            get { return stream_.CanTimeout; }
        }

#endif

        #region Methods

        /// <summary>
        ///     Close the stream.
        /// </summary>
        /// <remarks>
        ///     The underlying stream is closed only if <see cref="IsStreamOwner" /> is true.
        /// </remarks>
        public override void Close()
        {
            Stream toClose = stream_;
            stream_ = null;
            if (isOwner_ && (toClose != null))
            {
                isOwner_ = false;
                toClose.Close();
            }
        }

        public override void Flush()
        {
            stream_.Flush();
        }

        /// <summary>
        ///     Locates a block with the desired <paramref name="signature" />.
        /// </summary>
        /// <param name="signature"> The signature to find. </param>
        /// <param name="endLocation"> Location, marking the end of block. </param>
        /// <param name="minimumBlockSize"> Minimum size of the block. </param>
        /// <param name="maximumVariableData"> The maximum variable data. </param>
        /// <returns> Eeturns the offset of the first byte after the signature; -1 if not found </returns>
        public long LocateBlockWithSignature(int signature, long endLocation, int minimumBlockSize,
                                             int maximumVariableData)
        {
            long pos = endLocation - minimumBlockSize;
            if (pos < 0)
            {
                return -1;
            }

            long giveUpMarker = Math.Max(pos - maximumVariableData, 0);

            do
            {
                if (pos < giveUpMarker)
                {
                    return -1;
                }
                Seek(pos--, SeekOrigin.Begin);
            } while (ReadLEInt() != signature);

            return Position;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            return stream_.Read(buffer, offset, count);
        }

        /// <summary>
        ///     Read data descriptor at the end of compressed data.
        /// </summary>
        /// <param name="zip64"> if set to <c>true</c> [zip64]. </param>
        /// <param name="data"> The data to fill in. </param>
        /// <returns> Returns the number of bytes read in the descriptor. </returns>
        public void ReadDataDescriptor(bool zip64, DescriptorData data)
        {
            int intValue = ReadLEInt();

            // In theory this may not be a descriptor according to PKZIP appnote.
            // In practise its always there.
            if (intValue != ZipConstants.DataDescriptorSignature)
            {
                throw new ZipException("Data descriptor signature not found");
            }

            data.Crc = ReadLEInt();

            if (zip64)
            {
                data.CompressedSize = ReadLELong();
                data.Size = ReadLELong();
            }
            else
            {
                data.CompressedSize = ReadLEInt();
                data.Size = ReadLEInt();
            }
        }

        /// <summary>
        ///     Read an int in little endian byte order.
        /// </summary>
        /// <returns> Returns the value read. </returns>
        /// <exception cref="IOException">An i/o error occurs.</exception>
        /// <exception cref="System.IO.EndOfStreamException">The file ends prematurely</exception>
        public int ReadLEInt()
        {
            return ReadLEShort() | (ReadLEShort() << 16);
        }

        /// <summary>
        ///     Read a long in little endian byte order.
        /// </summary>
        /// <returns> The value read. </returns>
        public long ReadLELong()
        {
            return (uint) ReadLEInt() | ((long) ReadLEInt() << 32);
        }

        /// <summary>
        ///     Read an unsigned short in little endian byte order.
        /// </summary>
        /// <returns> Returns the value read. </returns>
        /// <exception cref="IOException">An i/o error occurs.</exception>
        /// <exception cref="EndOfStreamException">The file ends prematurely</exception>
        public int ReadLEShort()
        {
            int byteValue1 = stream_.ReadByte();

            if (byteValue1 < 0)
            {
                throw new EndOfStreamException();
            }

            int byteValue2 = stream_.ReadByte();
            if (byteValue2 < 0)
            {
                throw new EndOfStreamException();
            }

            return byteValue1 | (byteValue2 << 8);
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return stream_.Seek(offset, origin);
        }

        public override void SetLength(long value)
        {
            stream_.SetLength(value);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            stream_.Write(buffer, offset, count);
        }

        /// <summary>
        ///     Write a data descriptor.
        /// </summary>
        /// <param name="entry"> The entry to write a descriptor for. </param>
        /// <returns> Returns the number of descriptor bytes written. </returns>
        public int WriteDataDescriptor(ZipEntry entry)
        {
            if (entry == null)
            {
                throw new ArgumentNullException("entry");
            }

            int result = 0;

            // Add data descriptor if flagged as required
            if ((entry.Flags & (int) GeneralBitFlags.Descriptor) != 0)
            {
                // The signature is not PKZIP originally but is now described as optional
                // in the PKZIP Appnote documenting trhe format.
                WriteLEInt(ZipConstants.DataDescriptorSignature);
                WriteLEInt(unchecked((int) (entry.Crc)));

                result += 8;

                if (entry.LocalHeaderRequiresZip64)
                {
                    WriteLELong(entry.CompressedSize);
                    WriteLELong(entry.Size);
                    result += 16;
                }
                else
                {
                    WriteLEInt((int) entry.CompressedSize);
                    WriteLEInt((int) entry.Size);
                    result += 8;
                }
            }

            return result;
        }

        /// <summary>
        ///     Write the required records to end the central directory.
        /// </summary>
        /// <param name="noOfEntries"> The number of entries in the directory. </param>
        /// <param name="sizeEntries"> The size of the entries in the directory. </param>
        /// <param name="startOfCentralDirectory"> The start of the central directory. </param>
        /// <param name="comment"> The archive comment. (This can be null). </param>
        public void WriteEndOfCentralDirectory(long noOfEntries, long sizeEntries,
                                               long startOfCentralDirectory, byte[] comment)
        {
            if ((noOfEntries >= 0xffff) ||
                (startOfCentralDirectory >= 0xffffffff) ||
                (sizeEntries >= 0xffffffff))
            {
                WriteZip64EndOfCentralDirectory(noOfEntries, sizeEntries, startOfCentralDirectory);
            }

            WriteLEInt(ZipConstants.EndOfCentralDirectorySignature);

            WriteLEShort(0); // number of this disk
            WriteLEShort(0); // no of disk with start of central dir

            // Number of entries
            if (noOfEntries >= 0xffff)
            {
                WriteLEUshort(0xffff); // Zip64 marker
                WriteLEUshort(0xffff);
            }
            else
            {
                WriteLEShort((short) noOfEntries); // entries in central dir for this disk
                WriteLEShort((short) noOfEntries); // total entries in central directory
            }

            // Size of the central directory
            if (sizeEntries >= 0xffffffff)
            {
                WriteLEUint(0xffffffff); // Zip64 marker
            }
            else
            {
                WriteLEInt((int) sizeEntries);
            }

            // offset of start of central directory
            if (startOfCentralDirectory >= 0xffffffff)
            {
                WriteLEUint(0xffffffff); // Zip64 marker
            }
            else
            {
                WriteLEInt((int) startOfCentralDirectory);
            }

            int commentLength = (comment != null) ? comment.Length : 0;

            if (commentLength > 0xffff)
            {
                throw new ZipException(string.Format("Comment length({0}) is too long can only be 64K", commentLength));
            }

            WriteLEShort(commentLength);

            if (commentLength > 0)
            {
                Write(comment, 0, comment.Length);
            }
        }

        /// <summary>
        ///     Write an int in little endian byte order.
        /// </summary>
        /// <param name="value"> The value to write. </param>
        public void WriteLEInt(int value)
        {
            WriteLEShort(value);
            WriteLEShort(value >> 16);
        }

        /// <summary>
        ///     Write a long in little endian byte order.
        /// </summary>
        /// <param name="value"> The value to write. </param>
        public void WriteLELong(long value)
        {
            WriteLEInt((int) value);
            WriteLEInt((int) (value >> 32));
        }

        /// <summary>
        ///     Write an unsigned short in little endian byte order.
        /// </summary>
        /// <param name="value"> The value to write. </param>
        public void WriteLEShort(int value)
        {
            stream_.WriteByte((byte) (value & 0xff));
            stream_.WriteByte((byte) ((value >> 8) & 0xff));
        }

        /// <summary>
        ///     Write a uint in little endian byte order.
        /// </summary>
        /// <param name="value"> The value to write. </param>
        public void WriteLEUint(uint value)
        {
            WriteLEUshort((ushort) (value & 0xffff));
            WriteLEUshort((ushort) (value >> 16));
        }

        /// <summary>
        ///     Write a ulong in little endian byte order.
        /// </summary>
        /// <param name="value"> The value to write. </param>
        public void WriteLEUlong(ulong value)
        {
            WriteLEUint((uint) (value & 0xffffffff));
            WriteLEUint((uint) (value >> 32));
        }

        /// <summary>
        ///     Write a ushort in little endian byte order.
        /// </summary>
        /// <param name="value"> The value to write. </param>
        public void WriteLEUshort(ushort value)
        {
            stream_.WriteByte((byte) (value & 0xff));
            stream_.WriteByte((byte) (value >> 8));
        }

        /// <summary>
        ///     Write Zip64 end of central directory records (File header and locator).
        /// </summary>
        /// <param name="noOfEntries"> The number of entries in the central directory. </param>
        /// <param name="sizeEntries"> The size of entries in the central directory. </param>
        /// <param name="centralDirOffset"> The offset of the dentral directory. </param>
        public void WriteZip64EndOfCentralDirectory(long noOfEntries, long sizeEntries, long centralDirOffset)
        {
            long centralSignatureOffset = stream_.Position;
            WriteLEInt(ZipConstants.Zip64CentralFileHeaderSignature);
            WriteLELong(44); // Size of this record (total size of remaining fields in header or full size - 12)
            WriteLEShort(ZipConstants.VersionMadeBy); // Version made by
            WriteLEShort(ZipConstants.VersionZip64); // Version to extract
            WriteLEInt(0); // Number of this disk
            WriteLEInt(0); // number of the disk with the start of the central directory
            WriteLELong(noOfEntries); // No of entries on this disk
            WriteLELong(noOfEntries); // Total No of entries in central directory
            WriteLELong(sizeEntries); // Size of the central directory
            WriteLELong(centralDirOffset); // offset of start of central directory
            // zip64 extensible data sector not catered for here (variable size)

            // Write the Zip64 end of central directory locator
            WriteLEInt(ZipConstants.Zip64CentralDirLocatorSignature);

            // no of the disk with the start of the zip64 end of central directory
            WriteLEInt(0);

            // relative offset of the zip64 end of central directory record
            WriteLELong(centralSignatureOffset);

            // total number of disks
            WriteLEInt(1);
        }

        // Write the local file header
        private void WriteLocalHeader(ZipEntry entry, EntryPatchData patchData)
        {
            CompressionMethod method = entry.CompressionMethod;
            bool headerInfoAvailable = true; // How to get this?
            bool patchEntryHeader = false;

            WriteLEInt(ZipConstants.LocalHeaderSignature);

            WriteLEShort(entry.Version);
            WriteLEShort(entry.Flags);
            WriteLEShort((byte) method);
            WriteLEInt((int) entry.DosTime);

            if (headerInfoAvailable)
            {
                WriteLEInt((int) entry.Crc);
                if (entry.LocalHeaderRequiresZip64)
                {
                    WriteLEInt(-1);
                    WriteLEInt(-1);
                }
                else
                {
                    WriteLEInt(entry.IsCrypted
                                   ? (int) entry.CompressedSize + ZipConstants.CryptoHeaderSize
                                   : (int) entry.CompressedSize);
                    WriteLEInt((int) entry.Size);
                }
            }
            else
            {
                if (patchData != null)
                {
                    patchData.CrcPatchOffset = stream_.Position;
                }
                WriteLEInt(0); // Crc

                if (patchData != null)
                {
                    patchData.SizePatchOffset = stream_.Position;
                }

                // For local header both sizes appear in Zip64 Extended Information
                if (entry.LocalHeaderRequiresZip64 && patchEntryHeader)
                {
                    WriteLEInt(-1);
                    WriteLEInt(-1);
                }
                else
                {
                    WriteLEInt(0); // Compressed size
                    WriteLEInt(0); // Uncompressed size
                }
            }

            byte[] name = ZipConstants.ConvertToArray(entry.Flags, entry.Name);

            if (name.Length > 0xFFFF)
            {
                throw new ZipException("Entry name too long.");
            }

            var ed = new ZipExtraData(entry.ExtraData);

            if (entry.LocalHeaderRequiresZip64 && (headerInfoAvailable || patchEntryHeader))
            {
                ed.StartNewEntry();
                if (headerInfoAvailable)
                {
                    ed.AddLeLong(entry.Size);
                    ed.AddLeLong(entry.CompressedSize);
                }
                else
                {
                    ed.AddLeLong(-1);
                    ed.AddLeLong(-1);
                }
                ed.AddNewEntry(1);

                if (!ed.Find(1))
                {
                    throw new ZipException("Internal error cant find extra data");
                }

                if (patchData != null)
                {
                    patchData.SizePatchOffset = ed.CurrentReadIndex;
                }
            }
            else
            {
                ed.Delete(1);
            }

            byte[] extra = ed.GetEntryData();

            WriteLEShort(name.Length);
            WriteLEShort(extra.Length);

            if (name.Length > 0)
            {
                stream_.Write(name, 0, name.Length);
            }

            if (entry.LocalHeaderRequiresZip64 && patchEntryHeader)
            {
                patchData.SizePatchOffset += stream_.Position;
            }

            if (extra.Length > 0)
            {
                stream_.Write(extra, 0, extra.Length);
            }
        }

        #endregion Methods
    }
}